Skip to content

Has permission hook#5

Open
elshafei-developer wants to merge 17 commits into
test-claudefrom
has_permission-hook
Open

Has permission hook#5
elshafei-developer wants to merge 17 commits into
test-claudefrom
has_permission-hook

Conversation

@elshafei-developer

Copy link
Copy Markdown
Owner

Please provide enough information so that others can review your pull request:

Explain the details for making this change. What existing problem does the pull request solve?

Screenshots/GIFs

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1a4f375-4e49-4029-8fbf-ebbbf70de4d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch has_permission-hook

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@elshafei-developer

Copy link
Copy Markdown
Owner Author

@claude check this PR

@elshafei-developer

Copy link
Copy Markdown
Owner Author

Code review

Found 2 issues:

  1. Hook returning None (no-opinion) is now treated as a hard deny — breaking existing hook contracts

The original loop used if controller_permission is not None: return bool(controller_permission), which skips hooks that return None and only denies on an explicit falsy non-None value. The new loop uses if not controller_permission: return bool(controller_permission), so any hook that returns None (the conventional "I have no opinion, let the next hook decide") will now immediately return False and deny access. Every existing has_permission hook that returns None or omits a return statement will silently break.

org_permissions = copy.deepcopy(permissions)
for method in reversed(methods):
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
if not controller_permission:
return bool(controller_permission)

  1. Denial by a later hook can be undone by an earlier hook in the same loop — anti-escalation snapshot is not cumulative

org_permissions is captured once before the loop. The guard if org_permissions.get(key) == 0 and value == 1 only blocks re-enabling a bit that was originally 0 before any hook ran. If hook B (executed first, because reversed) sets permissions["delete"] = 0, hook A (executed second) can freely set it back to 1 — because org_permissions.get("delete") is 1, not 0, so the guard does not fire. The fix is to maintain a cumulative denied_keys set that grows as hooks deny bits, and enforce it after every hook call.

hooks = frappe.get_hooks("has_permission")
methods = hooks.get(doc.doctype, []) + hooks.get("*", [])
org_permissions = copy.deepcopy(permissions)
for method in reversed(methods):
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@elshafei-developer

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. Anti-escalation guard fails to revert permission keys newly added by hooks — a hook can set a permission key that was absent from the original role-based permissions (e.g. permissions["delete"] = 1 when delete was not granted), and the guard will not revert it because org_permissions.get(key) returns None, not 0, so None == 0 is False. This directly violates the stated invariant in the docstring: "Controllers can only deny permission, they can not explicitly grant any permission that wasn't already present." The guard should check org_permissions.get(key, 0) == 0 (or equivalently, check key not in org_permissions or org_permissions[key] == 0) to cover keys absent from the original dict.

methods = hooks.get(doc.doctype, []) + hooks.get("*", [])
org_permissions = copy.deepcopy(permissions)
for method in reversed(methods):
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")

🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

@elshafei-developer

Copy link
Copy Markdown
Owner Author

@claude can fix the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant